package com.j256.ormlite.hmos;

import com.j256.ormlite.dao.ObjectCache;
import com.j256.ormlite.field.FieldType;
import com.j256.ormlite.field.SqlType;
import com.j256.ormlite.logger.Logger;
import com.j256.ormlite.logger.LoggerFactory;
import com.j256.ormlite.misc.IOUtils;
import com.j256.ormlite.misc.SqlExceptionUtil;
import com.j256.ormlite.stmt.GenericRowMapper;
import com.j256.ormlite.stmt.StatementBuilder.StatementType;
import com.j256.ormlite.support.CompiledStatement;
import com.j256.ormlite.support.DatabaseConnection;
import com.j256.ormlite.support.GeneratedKeyHolder;

import ohos.data.rdb.RdbStore;
import ohos.data.rdb.Statement;
import ohos.data.resultset.ResultSet;

import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Savepoint;

/**
 * HmosDatabaseConnection
 *
 */
public class HmosDatabaseConnection implements DatabaseConnection {
    private static Logger logger = LoggerFactory.getLogger(HmosDatabaseConnection.class);
    private static final String[] NO_STRING_ARGS = new String[0];

    private final RdbStore db;
    private final boolean isReadWrite;
    private final boolean isCancelQueriesEnabled;

    /**
     * HmosDatabaseConnection
     *
     * @param db RdbStore
     * @param isreadWrite Boolean
     */
    public HmosDatabaseConnection(RdbStore db, boolean isreadWrite) {
        this(db, isreadWrite, false);
    }

    /**
     * HmosDatabaseConnection
     *
     * @param db RdbStore
     * @param isReadWrite Boolean
     * @param isCancelQueriesEnabled Boolean
     */
    public HmosDatabaseConnection(RdbStore db, boolean isReadWrite, boolean isCancelQueriesEnabled) {
        this.db = db;
        this.isReadWrite = isReadWrite;
        this.isCancelQueriesEnabled = isCancelQueriesEnabled;
        logger.trace("{}: db {} opened, read-write = {}", this, db, isReadWrite);
    }

    @Override
    public boolean isAutoCommitSupported() {
        return true;
    }

    @Override
    public boolean isAutoCommit() throws SQLException {
        return true;
    }

    @Override
    public void setAutoCommit(boolean isautoCommit) {
    }

    @Override
    public Savepoint setSavePoint(String name) throws SQLException {
        db.beginTransaction();
        logger.trace("{}: save-point set with name {}", this, name);
        return new OurSavePoint(name);
    }

    /**
     * Return whether this connection is read-write or not (real-only).
     *
     * @return readWrite
     */
    public boolean isReadWrite() {
        return isReadWrite;
    }

    @Override
    public void commit(Savepoint savepoint) throws SQLException {
        try {
            db.endTransaction();
            if (savepoint == null) {
                logger.trace("{}: transaction is successfully ended", this);
            } else {
                logger.trace("{}: transaction {} is successfully ended", this, savepoint.getSavepointName());
            }
        } catch (SQLException e) {
            if (savepoint == null) {
                throw SqlExceptionUtil.create("problems committing transaction", e);
            } else {
                throw SqlExceptionUtil.create("problems committing transaction " + savepoint.getSavepointName(), e);
            }
        }
    }

    @Override
    public void rollback(Savepoint savepoint) throws SQLException {
        try {
            db.endTransaction();
            if (savepoint == null) {
                logger.trace("{}: transaction is ended, unsuccessfully", this);
            } else {
                logger.trace("{}: transaction {} is ended, unsuccessfully",
                        this, savepoint.getSavepointName());
            }
        } catch (SQLException e) {
            if (savepoint == null) {
                throw SqlExceptionUtil.create("problems rolling back transaction", e);
            } else {
                throw SqlExceptionUtil.create("problems rolling back transaction " + savepoint.getSavepointName(), e);
            }
        }
    }

    @Override
    public void releaseSavePoint(Savepoint savePoint) {
        // noop
    }

    @Override
    public int executeStatement(String statementStr, int resultFlags) throws SQLException {
        return HmosCompiledStatement.execSql(db, statementStr, statementStr, NO_STRING_ARGS);
    }

    @Override
    public CompiledStatement compileStatement(
            String statement, StatementType type, FieldType[] argFieldTypes, int resultFlags, boolean cacheStore) {
        CompiledStatement stmt = new HmosCompiledStatement(statement, db, type, isCancelQueriesEnabled, cacheStore);
        logger.trace("{}: compiled statement got {}: {}", this, stmt, statement);
        return stmt;
    }

    @Override
    public int insert(String statement, Object[] args, FieldType[] argFieldTypes, GeneratedKeyHolder keyHolder)
            throws SQLException {
        Statement stmt = null;
        try {
            stmt = db.buildStatement(statement);
            bindArgs(stmt, args, argFieldTypes);
            long rowId = stmt.executeAndGetLastInsertRowId();
            if (keyHolder != null) {
                keyHolder.addKey(rowId);
            }
            int result = 1;
            logger.trace("{}: insert statement is compiled and executed, changed {}: {}", this, result, statement);
            return result;
        } catch (SQLException e) {
            throw SqlExceptionUtil.create("inserting to database failed: " + statement, e);
        } finally {
            closeQuietly(stmt);
        }
    }

    private int update(String statement, Object[] args, FieldType[] argFieldTypes, String label) throws SQLException {
        Statement stmt = null;
        try {
            stmt = db.buildStatement(statement);
            bindArgs(stmt, args, argFieldTypes);
            stmt.execute();
        } catch (SQLException e) {
            throw SqlExceptionUtil.create("updating database failed: " + statement, e);
        } finally {
            closeQuietly(stmt);
            stmt = null;
        }
        int result;
        try {
            stmt = db.buildStatement("SELECT CHANGES()");
            result = (int) stmt.executeAndGetLong();
        } finally {
            closeQuietly(stmt);
        }
        logger.trace("{} statement is compiled and executed, changed {}: {}", label, result, statement);
        return result;
    }

    /**
     * closeQuietly
     *
     * @param resultSet ResultSet
     */
    private void closeQuietly(ResultSet resultSet) {
        if (resultSet != null) {
            resultSet.close();
        }
    }

    @Override
    public int update(String statement, Object[] args, FieldType[] argFieldTypes) throws SQLException {
        return update(statement, args, argFieldTypes, "updated");
    }

    @Override
    public int delete(String statement, Object[] args, FieldType[] argFieldTypes) throws SQLException {
        return update(statement, args, argFieldTypes, "deleted");
    }

    @Override
    public <T> Object queryForOne(String statement, Object[] args, FieldType[] argFieldTypes,
                                  GenericRowMapper<T> rowMapper, ObjectCache objectCache) throws SQLException {
        ResultSet cursor = null;
        HmosDatabaseResults results = null;
        try {
            cursor = db.querySql(statement, toStrings(args));
            results = new HmosDatabaseResults(cursor, objectCache, true);
            logger.trace("{}: queried for one result: {}", this, statement);
            if (!results.first()) {
                return null;
            } else {
                T first = rowMapper.mapRow(results);
                if (results.next()) {
                    return MORE_THAN_ONE;
                } else {
                    return first;
                }
            }
        } finally {
            IOUtils.closeQuietly(results);
            closeQuietly(cursor);
        }
    }

    @Override
    public long queryForLong(String statement) throws SQLException {
        return 0;
    }

    @Override
    public long queryForLong(String statement, Object[] args, FieldType[] argFieldTypes) throws SQLException {
        return 0;
    }

    @Override
    public void close() throws IOException {
    }

    /**
     * closeQuietly
     *
     * @param statement Statement
     */
    private void closeQuietly(Statement statement) {
        if (statement != null) {
            statement.close();
        }
    }

    @Override
    public void closeQuietly() {
        IOUtils.closeQuietly(this);
    }

    @Override
    public boolean isClosed() throws SQLException {
        return false;
    }

    @Override
    public boolean isTableExists(String tableName) {
        ResultSet resultSet = db.querySql("SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name = ?",
                new String[]{tableName});

        return false;
    }

    @Override
    public Connection getUnderlyingConnection() {
        return null;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "@" + Integer.toHexString(super.hashCode());
    }

    private void bindArgs(Statement stmt, Object[] args, FieldType[] argFieldTypes) throws SQLException {
        if (args == null) {
            return;
        }
        for (int index = 0; index < args.length; index++) {
            Object arg = args[index];
            if (arg == null) {
                stmt.setNull(index + 1);
            } else {
                SqlType sqlType = argFieldTypes[index].getSqlType();
                switch (sqlType) {
                    case STRING:
                    case LONG_STRING:
                    case CHAR:
                        stmt.setString(index + 1, arg.toString());
                        break;
                    case BOOLEAN:
                    case BYTE:
                    case SHORT:
                    case INTEGER:
                    case LONG:
                        stmt.setLong(index + 1, ((Number) arg).longValue());
                        break;
                    case FLOAT:
                    case DOUBLE:
                        stmt.setDouble(index + 1, ((Number) arg).doubleValue());
                        break;
                    case BYTE_ARRAY:
                    case SERIALIZABLE:
                        stmt.setBlob(index + 1, (byte[]) arg);
                        break;
                    case DATE:
                    case BLOB:
                    case BIG_DECIMAL:
                        throw new SQLException("Invalid type: " + sqlType);
                    case UNKNOWN:
                    default:
                        throw new SQLException("Unknown sql argument type: " + sqlType);
                }
            }
        }
    }

    private String[] toStrings(Object[] args) {
        if (args == null || args.length == 0) {
            return new String[0];
        }
        String[] strings = new String[args.length];
        for (int index = 0; index < args.length; index++) {
            Object arg = args[index];
            if (arg == null) {
                strings[index] = null;
            } else {
                strings[index] = arg.toString();
            }
        }
        return strings;
    }

    /**
     * HmosDatabaseConnection
     *
     */
    private static class OurSavePoint implements Savepoint {
        private String name;

        /**
         * OurSavePoint
         *
         * @param name String
         */
        public OurSavePoint(String name) {
            this.name = name;
        }

        @Override
        public int getSavepointId() {
            return 0;
        }

        @Override
        public String getSavepointName() {
            return name;
        }
    }
}
